package org.apache.dubbo.config;

import static org.apache.dubbo.common.constants.CommonConstants.ANY_VALUE;
import static org.apache.dubbo.common.constants.CommonConstants.CLUSTER_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SEPARATOR;
import static org.apache.dubbo.common.constants.CommonConstants.CONSUMER_SIDE;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_METADATA_STORAGE_TYPE;
import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.LOCALHOST_VALUE;
import static org.apache.dubbo.common.constants.CommonConstants.METADATA_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.METHODS_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.MONITOR_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.PROXY_CLASS_REF;
import static org.apache.dubbo.common.constants.CommonConstants.REMOTE_METADATA_STORAGE_TYPE;
import static org.apache.dubbo.common.constants.CommonConstants.REVISION_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.SEMICOLON_SPLIT_PATTERN;
import static org.apache.dubbo.common.constants.CommonConstants.SIDE_KEY;
import static org.apache.dubbo.common.utils.NetUtils.isInvalidLocalHost;
import static org.apache.dubbo.config.Constants.DUBBO_IP_TO_REGISTRY;
import static org.apache.dubbo.registry.Constants.CONSUMER_PROTOCOL;
import static org.apache.dubbo.registry.Constants.REGISTER_IP_KEY;
import static org.apache.dubbo.rpc.Constants.LOCAL_PROTOCOL;
import static org.apache.dubbo.rpc.cluster.Constants.REFER_KEY;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.Version;
import org.apache.dubbo.common.bytecode.Wrapper;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.ConfigUtils;
import org.apache.dubbo.common.utils.NetUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.common.utils.UrlUtils;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.config.bootstrap.DubboBootstrap;
import org.apache.dubbo.config.event.ReferenceConfigDestroyedEvent;
import org.apache.dubbo.config.event.ReferenceConfigInitializedEvent;
import org.apache.dubbo.config.utils.ConfigValidationUtils;
import org.apache.dubbo.event.Event;
import org.apache.dubbo.event.EventDispatcher;
import org.apache.dubbo.metadata.WritableMetadataService;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Protocol;
import org.apache.dubbo.rpc.ProxyFactory;
import org.apache.dubbo.rpc.cluster.Cluster;
import org.apache.dubbo.rpc.cluster.directory.StaticDirectory;
import org.apache.dubbo.rpc.cluster.support.ClusterUtils;
import org.apache.dubbo.rpc.cluster.support.registry.ZoneAwareCluster;
import org.apache.dubbo.rpc.model.ApplicationModel;
import org.apache.dubbo.rpc.model.AsyncMethodInfo;
import org.apache.dubbo.rpc.model.ConsumerModel;
import org.apache.dubbo.rpc.model.ServiceDescriptor;
import org.apache.dubbo.rpc.model.ServiceRepository;
import org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol;
import org.apache.dubbo.rpc.service.GenericService;
import org.apache.dubbo.rpc.support.ProtocolUtils;

/**
 * 来源原版 ReferenceConfig
 * 
 * @author lifeng
 * @date 2020年9月6日 下午5:51:08
 */
@SuppressWarnings("deprecation")
public class ReferenceConfig<T> extends ReferenceConfigBase<T> {

	private static final long serialVersionUID = 1L;

	public static final Logger logger = LoggerFactory.getLogger(ReferenceConfig.class);

	/**
	 * The {@link Protocol} implementation with adaptive functionality,it will be
	 * different in different scenarios. A particular {@link Protocol}
	 * implementation is determined by the protocol attribute in the {@link URL}.
	 * For example:
	 *
	 * <li>when the url is
	 * registry://224.5.6.7:1234/org.apache.dubbo.registry.RegistryService?application=dubbo-sample,
	 * then the protocol is <b>RegistryProtocol</b></li>
	 *
	 * <li>when the url is
	 * dubbo://224.5.6.7:1234/org.apache.dubbo.config.api.DemoService?application=dubbo-sample,
	 * then the protocol is <b>DubboProtocol</b></li>
	 * <p>
	 * Actually，when the {@link ExtensionLoader} init the {@link Protocol}
	 * instants,it will automatically wraps two layers, and eventually will get a
	 * <b>ProtocolFilterWrapper</b> or <b>ProtocolListenerWrapper</b>
	 */
	private static final Protocol REF_PROTOCOL = ExtensionLoader.getExtensionLoader(Protocol.class)
			.getAdaptiveExtension();

	/**
	 * The {@link Cluster}'s implementation with adaptive functionality, and
	 * actually it will get a {@link Cluster}'s specific implementation who is
	 * wrapped with <b>MockClusterInvoker</b>
	 */
	private static final Cluster CLUSTER = ExtensionLoader.getExtensionLoader(Cluster.class).getAdaptiveExtension();

	/**
	 * A {@link ProxyFactory} implementation that will generate a reference
	 * service's proxy,the JavassistProxyFactory is its default implementation
	 */
	private static final ProxyFactory PROXY_FACTORY = ExtensionLoader.getExtensionLoader(ProxyFactory.class)
			.getAdaptiveExtension();

	/**
	 * The interface proxy reference
	 */
	private transient volatile T ref;

	/**
	 * The invoker of the reference service
	 */
	private transient volatile Invoker<?> invoker;

	/**
	 * The flag whether the ReferenceConfig has been initialized
	 */
	private transient volatile boolean initialized;

	/**
	 * whether this ReferenceConfig has been destroyed
	 */
	private transient volatile boolean destroyed;

	private final ServiceRepository repository;

	private DubboBootstrap bootstrap;

	public ReferenceConfig() {
		super();
		this.repository = ApplicationModel.getServiceRepository();
	}

	public ReferenceConfig(Reference reference) {
		super(reference);
		this.repository = ApplicationModel.getServiceRepository();
	}

	public synchronized T get() {
		if (destroyed) {
			throw new IllegalStateException("The invoker of ReferenceConfig(" + url + ") has already destroyed!");
		}
		if (ref == null) {
			init();
		}
		return ref;
	}

	public synchronized void destroy() {
		if (ref == null) {
			return;
		}
		if (destroyed) {
			return;
		}
		destroyed = true;
		try {
			invoker.destroy();
		} catch (Throwable t) {
			logger.warn("Unexpected error occured when destroy invoker of ReferenceConfig(" + url + ").", t);
		}
		invoker = null;
		ref = null;

		// dispatch a ReferenceConfigDestroyedEvent since 2.7.4
		dispatch(new ReferenceConfigDestroyedEvent(this));
	}

	public synchronized void init() {
		if (initialized) {
			return;
		}

		if (bootstrap == null) {
			bootstrap = DubboBootstrap.getInstance();
			bootstrap.init();
		}

		checkAndUpdateSubConfigs();

		checkStubAndLocal(interfaceClass);
		ConfigValidationUtils.checkMock(interfaceClass, this);

		Map<String, String> map = new HashMap<String, String>();
		map.put(SIDE_KEY, CONSUMER_SIDE);

		ReferenceConfigBase.appendRuntimeParameters(map);
		if (!ProtocolUtils.isGeneric(generic)) {
			String revision = Version.getVersion(interfaceClass, version);
			if (revision != null && revision.length() > 0) {
				map.put(REVISION_KEY, revision);
			}

			String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
			if (methods.length == 0) {
				logger.warn("No method found in service interface " + interfaceClass.getName());
				map.put(METHODS_KEY, ANY_VALUE);
			} else {
				map.put(METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), COMMA_SEPARATOR));
			}
		}
		map.put(INTERFACE_KEY, interfaceName);
		AbstractConfig.appendParameters(map, getMetrics());
		AbstractConfig.appendParameters(map, getApplication());
		AbstractConfig.appendParameters(map, getModule());
		// remove 'default.' prefix for configs from ConsumerConfig
		// appendParameters(map, consumer, Constants.DEFAULT_KEY);
		AbstractConfig.appendParameters(map, consumer);
		AbstractConfig.appendParameters(map, this);
		MetadataReportConfig metadataReportConfig = getMetadataReportConfig();
		if (metadataReportConfig != null && metadataReportConfig.isValid()) {
			map.putIfAbsent(METADATA_KEY, REMOTE_METADATA_STORAGE_TYPE);
		}
		Map<String, AsyncMethodInfo> attributes = null;
		if (CollectionUtils.isNotEmpty(getMethods())) {
			attributes = new HashMap<>();
			for (MethodConfig methodConfig : getMethods()) {
				AbstractConfig.appendParameters(map, methodConfig, methodConfig.getName());
				String retryKey = methodConfig.getName() + ".retry";
				if (map.containsKey(retryKey)) {
					String retryValue = map.remove(retryKey);
					if ("false".equals(retryValue)) {
						map.put(methodConfig.getName() + ".retries", "0");
					}
				}
				AsyncMethodInfo asyncMethodInfo = AbstractConfig.convertMethodConfig2AsyncInfo(methodConfig);
				if (asyncMethodInfo != null) {
//                    consumerModel.getMethodModel(methodConfig.getName()).addAttribute(ASYNC_KEY, asyncMethodInfo);
					attributes.put(methodConfig.getName(), asyncMethodInfo);
				}
			}
		}

		String hostToRegistry = ConfigUtils.getSystemProperty(DUBBO_IP_TO_REGISTRY);
		if (StringUtils.isEmpty(hostToRegistry)) {
			hostToRegistry = NetUtils.getLocalHost();
		} else if (isInvalidLocalHost(hostToRegistry)) {
			throw new IllegalArgumentException("Specified invalid registry ip from property:" + DUBBO_IP_TO_REGISTRY
					+ ", value:" + hostToRegistry);
		}
		map.put(REGISTER_IP_KEY, hostToRegistry);

		serviceMetadata.getAttachments().putAll(map);

		ref = createProxy(map);

		serviceMetadata.setTarget(ref);
		serviceMetadata.addAttribute(PROXY_CLASS_REF, ref);
		ConsumerModel consumerModel = repository.lookupReferredService(serviceMetadata.getServiceKey());
		consumerModel.setProxyObject(ref);
		consumerModel.init(attributes);

		initialized = true;

		// dispatch a ReferenceConfigInitializedEvent since 2.7.4
		dispatch(new ReferenceConfigInitializedEvent(this, invoker));
	}

	@SuppressWarnings({ "unchecked", "rawtypes", })
	protected T createProxy(Map<String, String> map) {
		if (shouldJvmRefer(map)) {
			URL url = new URL(LOCAL_PROTOCOL, LOCALHOST_VALUE, 0, interfaceClass.getName()).addParameters(map);
			invoker = REF_PROTOCOL.refer(interfaceClass, url);
			if (logger.isInfoEnabled()) {
				logger.info("Using injvm service " + interfaceClass.getName());
			}
		} else {
			urls.clear();
			if (url != null && url.length() > 0) { // user specified URL, could be peer-to-peer address, or register
													// center's address.
				String[] us = SEMICOLON_SPLIT_PATTERN.split(url);
				if (us != null && us.length > 0) {
					for (String u : us) {
						URL url = URL.valueOf(u);
						if (StringUtils.isEmpty(url.getPath())) {
							url = url.setPath(interfaceName);
						}
						if (UrlUtils.isRegistry(url)) {
							urls.add(url.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
						} else {
							urls.add(ClusterUtils.mergeUrl(url, map));
						}
					}
				}
			} else { // assemble URL from register center's configuration
				// if protocols not injvm checkRegistry
				if (!LOCAL_PROTOCOL.equalsIgnoreCase(getProtocol())) {
					checkRegistry();
					List<URL> us = ConfigValidationUtils.loadRegistries(this, false);
					if (CollectionUtils.isNotEmpty(us)) {
						for (URL u : us) {
							URL monitorUrl = ConfigValidationUtils.loadMonitor(this, u);
							if (monitorUrl != null) {
								map.put(MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
							}
							urls.add(u.addParameterAndEncoded(REFER_KEY, StringUtils.toQueryString(map)));
						}
					}
					if (urls.isEmpty()) {
						throw new IllegalStateException(
								"No such any registry to reference " + interfaceName + " on the consumer "
										+ NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion()
										+ ", please config <dubbo:registry address=\"...\" /> to your spring config.");
					}
				}
			}

			if (urls.size() == 1) {
				invoker = REF_PROTOCOL.refer(interfaceClass, urls.get(0));
			} else {
				List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
				URL registryURL = null;
				for (URL url : urls) {
					invokers.add(REF_PROTOCOL.refer(interfaceClass, url));
					if (UrlUtils.isRegistry(url)) {
						registryURL = url; // use last registry url
					}
				}
				if (registryURL != null) { // registry url is available
					// for multi-subscription scenario, use 'zone-aware' policy by default
					URL u = registryURL.addParameterIfAbsent(CLUSTER_KEY, ZoneAwareCluster.NAME);
					// The invoker wrap relation would be like:
					// ZoneAwareClusterInvoker(StaticDirectory) ->
					// FailoverClusterInvoker(RegistryDirectory, routing happens here) -> Invoker
					invoker = CLUSTER.join(new StaticDirectory(u, invokers));
				} else { // not a registry url, must be direct invoke.
					invoker = CLUSTER.join(new StaticDirectory(invokers));
				}
			}
		}

		if (shouldCheck() && !invoker.isAvailable()) {
			invoker.destroy();
			throw new IllegalStateException("Failed to check the status of the service " + interfaceName
					+ ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName
					+ (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer "
					+ NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
		}
		if (logger.isInfoEnabled()) {
			logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
		}
		/**
		 * @since 2.7.0 ServiceData Store
		 */
		String metadata = map.get(METADATA_KEY);
		WritableMetadataService metadataService = WritableMetadataService
				.getExtension(metadata == null ? DEFAULT_METADATA_STORAGE_TYPE : metadata);
		if (metadataService != null) {
			URL consumerURL = new URL(CONSUMER_PROTOCOL, map.remove(REGISTER_IP_KEY), 0, map.get(INTERFACE_KEY), map);
			metadataService.publishServiceDefinition(consumerURL);
		}
		// create service proxy
		return (T) PROXY_FACTORY.getProxy(invoker, ProtocolUtils.isGeneric(generic));
	}

	/**
	 * This method should be called right after the creation of this class's
	 * instance, before any property in other config modules is used. Check each
	 * config modules are created properly and override their properties if
	 * necessary.
	 */
	public void checkAndUpdateSubConfigs() {
		if (StringUtils.isEmpty(interfaceName)) {
			throw new IllegalStateException("<dubbo:reference interface=\"\" /> interface not allow null!");
		}
		completeCompoundConfigs(consumer);
		if (consumer != null) {
			if (StringUtils.isEmpty(registryIds)) {
				setRegistryIds(consumer.getRegistryIds());
			}
		}
		// get consumer's global configuration
		checkDefault();
		this.refresh();
		if (getGeneric() == null && getConsumer() != null) {
			setGeneric(getConsumer().getGeneric());
		}
		if (ProtocolUtils.isGeneric(generic)) {
			interfaceClass = GenericService.class;
		} else {
			try {
				interfaceClass = Class.forName(interfaceName, true, Thread.currentThread().getContextClassLoader());
			} catch (ClassNotFoundException e) {
				throw new IllegalStateException(e.getMessage(), e);
			}
			checkInterfaceAndMethods(interfaceClass, getMethods());
		}

		// init serivceMetadata
		serviceMetadata.setVersion(version);
		serviceMetadata.setGroup(group);
		serviceMetadata.setDefaultGroup(group);
		serviceMetadata.setServiceType(getActualInterface());
		serviceMetadata.setServiceInterfaceName(interfaceName);
		// TODO, uncomment this line once service key is unified
		serviceMetadata.setServiceKey(URL.buildKey(interfaceName, group, version));

		ServiceRepository repository = ApplicationModel.getServiceRepository();
		ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass);
		repository.registerConsumer(serviceMetadata.getServiceKey(), serviceDescriptor, this, null, serviceMetadata);

		resolveFile();
		ConfigValidationUtils.validateReferenceConfig(this);
		postProcessConfig();
	}

	/**
	 * Figure out should refer the service in the same JVM from configurations. The
	 * default behavior is true 1. if injvm is specified, then use it 2. then if a
	 * url is specified, then assume it's a remote call 3. otherwise, check scope
	 * parameter 4. if scope is not specified but the target service is provided in
	 * the same JVM, then prefer to make the local call, which is the default
	 * behavior
	 */
	protected boolean shouldJvmRefer(Map<String, String> map) {
		URL tmpUrl = new URL("temp", "localhost", 0, map);
		boolean isJvmRefer;
		if (isInjvm() == null) {
			// if a url is specified, don't do local reference
			if (url != null && url.length() > 0) {
				isJvmRefer = false;
			} else {
				// by default, reference local service if there is
				isJvmRefer = InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl);
			}
		} else {
			isJvmRefer = isInjvm();
		}
		return isJvmRefer;
	}

	/**
	 * Dispatch an {@link Event event}
	 *
	 * @param event an {@link Event event}
	 * @since 2.7.5
	 */
	protected void dispatch(Event event) {
		EventDispatcher.getDefaultExtension().dispatch(event);
	}

	public DubboBootstrap getBootstrap() {
		return bootstrap;
	}

	public void setBootstrap(DubboBootstrap bootstrap) {
		this.bootstrap = bootstrap;
	}

	private void postProcessConfig() {
		List<ConfigPostProcessor> configPostProcessors = ExtensionLoader.getExtensionLoader(ConfigPostProcessor.class)
				.getActivateExtension(URL.valueOf("configPostProcessor://"), (String[]) null);
		configPostProcessors.forEach(component -> component.postProcessReferConfig(this));
	}

	// just for test
	Invoker<?> getInvoker() {
		return invoker;
	}
}